Historically, the first form of database support in Visual Basic (version 3) was the Data control and bound controls, as I explained in the first section of this chapter. This binding mechanism has undergone several refinements in Visual Basic 4 and 5, but it hadn't changed much until the ADO binding mechanism made its debut in Visual Basic 6.
First let me briefly remind you that binding is a technology that lets you place controls—such as TextBox, CheckBox, ListBox, and ComboBox controls—on a form and bind any or all of them to another control, called the Data control, which in turn is connected to a database. The Data control allows you to navigate through the records in the database: Each time a new record becomes current, its field values appear in the bound controls. Similarly, if the user updates one or more values in the bound controls, these changes are propagated to the database. You can therefore create simple user interfaces to database data without writing a single line of code. (This is in theory; in practice you should validate input data and often format data displayed in bound controls.)
The new ADO-based binding technology is a revolution in how you display data from a database. First of all, you don't always have a database to work with, not directly at least. In Visual Basic 6 you shouldn't talk about bound controls and Data controls; instead, you should talk about one or more data consumers that are bound to a data source. In Visual Basic 6, you can use many types of data consumers, such as an intrinsic or external control, a class, a COM component, a homemade ActiveX control (or UserControl), or the DataReport designer. You also have many data sources to choose from: the ADO Data control, a class, a COM component, a UserControl, or the DataEnvironment designer. (The DataEnvironment designer is described later in this chapter, whereas the DataReport designer is covered in Chapter 15.)
This assortment of data sources and consumers gives you an unparalleled flexibility in devising the most appropriate binding scheme for your application, and it also overcomes one of the most serious limitations of the original Data control (and its younger cousin, the RemoteData control). When you use ADO binding, you're not tied to a 2-tier architecture because you don't have to necessarily bind user interface elements to fields in a database. Rather, you can use one or more intermediate COM components, which consistently implement a more flexible 3-tier design. It isn't really important where these COM components reside; they might execute on the client machine, on the server machine, or on another machine. Don't forget that n-tier development is a state of mind: In a sense, you can do 3-tier development even when the client and the server are on the same machine (although in that case you surely don't take advantage of many of the 3-tier promises). The important point in n-tier architectures is that the front-end application isn't tightly tied to the back-end database so that you can—if the need arises—substitute the front end or the back end without having to rewrite the entire application.
You exploit the simplest form of ADO binding when you bind one or more controls to an ADO Data control, but the concepts I'll explain here can be applied to any type of data source or consumer. And you'll learn more about writing such data sources and consumers in Chapters 17 and 18.
Before using the ADO Data control, you must add it to the current project to have it available in the control Toolbox. You also need to add some items to the References dialog box, so let's take the following shortcut: Select the New Project command from the File menu, and then open the Data Project template from the Project gallery. This template adds several modules to the project (some of which aren't of interest to us right now). Above all, the template adds a reference to the Microsoft ActiveX Data Objects 2.0 Library (MSADO15.DLL) and a number of additional controls to the Toolbox, including the ADO Data control. If you already upgraded to the new ADO 2.1 version, the item added to the References dialog box is msado20.tlb.
Drop an instance of the ADO Data control on the only form in the project, frmDataEnv, and set its Align property to 2-vbAlignBottom so that it resizes with the form. You might set other properties from the regular Properties window, but it's much better to use the custom Property pages that pop up when you right-click on the control and select the ADODC Properties menu command. In the General page, you can specify which database you're connecting to using three different methods: a Data Link file, an ODBC Data Source Name (the old DSNs aren't dead in the ADO world, as you can see), or a custom connection string.
Data link files are the ADO equivalent of file-based DSNs. At first, you might wonder how to create a UDL file because no New or Add button is immediately apparent. The answer is in Figure 8-13: Click on the Browse button to bring up an Open dialog box that lets you browse through your directories looking for UDL files. Then right-click inside the (possibly empty) file list part of the dialog box, and select the New submenu and the Microsoft Data Link menu command. This creates a "blank" UDL file, which you can rename whatever you like. It's a simple task once you know how to do it; just don't call this process an intuitive user interface!
Creating the UDL file is only part of the job because you now have to specify which database this Data Link file is pointing to. You need to perform another counter-intuitive operation: Right-click on the UDL file that you've just created, and select the Properties menu command. This brings up a multipage Properties dialog box, which seems intimidating at first. A closer look reveals that the last four tabs are nothing new: They're the Provider, Connection, Advanced, and All pages you saw when we created a data link inside the Data View window. This shouldn't be a surprise. We're still talking about the same concepts here. Anyway, it's good to walk in familiar territory again.
By the way, you can also do without UDL files and create the connection string directly. If you click on the Build button in the ADODC Properties dialog box, once again you'll see those four pages that let you define an ADO connection. Suddenly, everything you learned when you were working with the Data View window makes sense in this context as well. For this example, let's connect to the NWind.mdb database, using any of the available methods.
Figure 8-13. The long path to Data Link file creation.
Back to the ADODC Properties dialog box. Move on to the Authentication page, enter user name and password if your database requires them, and then click on the RecordSource page. This is where you define the database table, the stored procedure, or the SQL query that will feed data to bound controls. For this example, select the adCmdText option in the upper combo box, and then enter the following SQL query:
Select * from Products |
Go back to the form and add four TextBox controls, four Labels, and one CheckBox control, as shown in Figure 8-14. Set the DataSource properties of the CheckBox control and all the TextBox controls to Adodc1, and then set their DataField properties to the name of the database field you want to bind the control to. You don't have to guess their names because a handy drop-down list in the Properties window contains all the field names in the Publishers table. For this example, use the ProductName, UnitPrice, UnitsInStock, UnitsOnOrder, and Discontinued fields.
You're finally ready to run this program and to navigate through the records using the four arrow buttons in the ADO Data control. Try to modify one or more fields, and then move to the next record and back to see whether the new values have persisted. As you can see, the mechanism really works without your having written a single line of Visual Basic code.
You can even add new records, still without writing a single line of code. Go back to design time, and set the ADO Data control's EOFAction property to the value 2-adDoAddNew. Then run the sample program again, click on the rightmost arrow button to move to the last record, and click on the second button from the right to move to the next record. All bound controls will be cleared, and you can enter a new record's field values. Remember that you need to move to another record to make your edits persistent. If you simply close the form, any changes you made to the current record are lost.
Figure 8-14. The ADO Data control sample application uses the DataCombo control described in Chapter 15.
Most of the Visual Basic 6 intrinsic controls support data binding, including the TextBox, Label, CheckBox, ListBox, ComboBox, PictureBox, and OLE controls. Typically, you use TextBox for editable string or numeric fields, Labels for non-editable fields, CheckBoxes for Boolean values, and ListBoxes and ComboBoxes to list valid values. In the Visual Basic 6 package, you'll also find some external ActiveX controls that support data binding, such as the ImageCombo, MonthView, DateTimePicker, MaskEdBox, RichTextBox, DataGrid, DataList, DataCombo, and Hierarchical FlexGrid controls. In this section, I've assembled a few tips and suggestions for making the best use of the intrinsic controls as bound controls; data-aware ActiveX controls are covered in Chapters 10, 11, 12, and 15.
NOTE
Visual Basic 6 includes some data-aware controls—namely the DBGrid, DBList, DBCombo, and MSFlexGrid controls—that aren't compatible with the ADO Data control and work only with older Data and RemoteData controls. All the intrinsic controls work with both the older Data controls and the newer ADO Data control.
All bound controls expose the DataChanged run-time_only Boolean property. Visual Basic sets this property to True when the end user (or the code) modifies the value in the control. The ADO binding mechanism uses this flag to check whether the current record has been edited and resets it to False after displaying a new record. This means that you can prevent the ADO Data control from moving a control's value to a database field by setting the control's DataChanged property to False yourself.
TIP
The DataChanged property is independent from the ADO binding mechanism and is correctly managed from Visual Basic even if the control isn't currently bound to a database field. For example, you can exploit this property when you want to determine whether a control's contents have been modified after you have loaded a value in it. If you didn't use this property, you would need to use a form-level Boolean variable and manually set it to True from within the control's Change or Click event procedure.
When you use a data-bound Label control, you should set its UseMnemonics property to False. If it's set to its True (default) value, all ampersand characters in database fields will be mistakenly interpreted as placeholders for hot key characters.
The CheckBox control recognizes a zero value as vbUnchecked, any nonzero value as vbChecked, and a Null value as vbGrayed. But when you modify the value in this field, The CheckBox control always stores 0 and _1 values in the database. For this reason, the CheckBox control should be bound only to a Boolean field.
CAUTION
You can't bind a PictureBox control to photographs stored in an Access database field of type OLE control. Instead, you must use an OLE control. A bound PictureBox control is fine for displaying a photograph stored in an SQL Server field of type Image.
The OptionButton control isn't data aware, so you need to resort to the following trick to bind it to an ADO Data control. Create an array of OptionButton controls and a hidden TextBox control, and bind the hidden TextBox control to the database field in question. Then write the code you see at the top of the next page in the form module.
Private Sub optRadioButton_Click(Index As Integer) ' Change hidden TextBox's contents when user clicks on radio buttons. txtHidden.Text = Trim$(Index) End Sub Private Sub txtHidden_Change() ' Select the correct radio button when the ADO Data control ' assigns a new value to the hidden TextBox. On Error Resume Next optRadioButton(CInt(txtHidden.Text)).Value = True End Sub |
The ideal solution would be to have an ActiveX control that displays an array of OptionButton controls and bind them to a single database field, and in fact a few commercial controls do exactly this. But now that Visual Basic supports the creation of ActiveX controls, you can create such a control yourself in a few minutes. See Chapter 17 to learn how you can create bound ActiveX controls.
Pay attention when you use ComboBox controls with Style set to 2-DropdownList and ListBox controls as bound controls. If the value in the database doesn't match one of the values in the list, you get a run-time error.
Visual Basic 6 lets you assign the DataSource property of a control at run time, so you can bind (or unbind) a control during execution:
' Bind a control at run time. txtFirstName.DataField = "FirstName" Set txtFirstName.DataSource = Adodc1 ... ' Unbind it. Set txtFirstName.DataSource = Nothing |
Dynamic assignments of the DataSource property don't work with older Data and RemoteData controls.
The ADO Data control embodies many features of the ADO Connection and Recordset objects, which I'll cover in Chapter 13. For this reason, in this section I'll only describe the few properties that you really need to set up a minimal sample application that uses this control.
The ConnectionString property is the string that contains all the information that's necessary to complete the connection, as I showed you in the previous section. You can set login data using the UserName and Password properties, and you can define a timeout for the connection attempt with the ConnectionTimeout property. Mode determines which operations are allowed on the connection. For additional information about these properties, read "The Connection Object" section in Chapter 13.
Most of the other properties of the ADO Data control are borrowed from the ADO Recordset object. RecordSource is the table, the stored procedure, or the SQL command that returns the records from the database. (It corresponds to the Recordset object's Source property.) CommandType is the type of query stored in the RecordSource property, and CommandTimeout is the timeout in seconds for the command to execute. CursorLocation specifies whether the cursor should be located on the client or on the server workstation, and CursorType determines the type of the cursor. CacheSize is the number of records that are read from the database in each data transfer, whereas LockType affects how the client application can update data in the database. I examine these properties in detail, along with cursor and locking options, in Chapters 13 and 14.
The ADO Data control also exposes a pair of properties that have no corresponding items in the Recordset object. The EOFAction property determines what happens when the user attempts to move past the last record: 0-adDoMoveLast means that the record pointer stays at the last record in the Recordset, 1-adStayEOF sets the EOF (End-Of-File) condition, and 2-adDoAddNew automatically adds a new record. The BOFAction property determines what happens when the user clicks on the left arrow button when the first record is the current record; 0-adDoMoveFirst leaves the pointer on the first record, and 1-adStayBOF sets the BOF (Begin-Of-File) condition.
At run time, the ADO Data control exposes one more property, the Recordset property, which returns a reference to the underlying Recordset object. This reference is essential for exploiting all the power of this control because it lets you add support for other operations. For example, you can create two CommandButton controls, set their Caption properties to AddNew and Delete, and write this code behind their Click events:
Private Sub cmdAddNew_Click() Adodc1.Recordset.AddNew End Sub Private Sub cmdDelete_Click() Adodc1.Recordset.Delete End Sub |
Keep in mind, however, that the preceding operations aren't always permitted; for example, you can't add new records or delete existing ones if the ADO Data control has opened the data source in read-only mode. Even if the data source has been opened in read-write mode, certain operations on a record might be illegal. For example, you can't delete a record in the Customers table when one or more records in the Orders table are referencing it.
The Recordset object is packed with properties and methods, and you can use all of them from the ADO Data control. You can, for example, sort or filter the displayed records, or set a bookmark to quickly return to a given record. The ADO Data control doesn't expose the underlying Connection object directly, but you can use the ActiveConnection property of the underlying Recordset. For example, you can implement transactions using the following code:
Private Sub Form_Load() ' Start a transaction when the form loads. Adodc1.Recordset.ActiveConnection.BeginTrans End Sub Private Sub Form_Unload(Cancel As Integer) ' Commit or roll back when the form unloads. If MsgBox("Do you confirm changes to records?", vbYesNo) = vbYes Then Adodc1.Recordset.ActiveConnection.CommitTrans Else Adodc1.Recordset.ActiveConnection.RollbackTrans End If End Sub |
If you roll back a transaction using the preceding Form_Unload routine, you need to invoke the control's Refresh method to see the previous values in records.
NOTE
A transaction is a group of related database operations that should be logically considered as a single command, which can either succeed or fail as a whole. For example, when you move money from one bank account to another account you should enclose the two operations within a transaction so that if the second operation isn't successful, the first one is canceled as well. When you confirm a transaction, you should commit it. When you want to cancel the effect of all its commands, you should roll it back.
The ADO Data control also exposes several events borrowed from the Recordset, so I'm deferring the description of most of them until Chapter 13. I want to describe only three events here. The MoveComplete event fires when a new record has become the current record. You can exploit this event to display information that can't be retrieved with a simple bound control. For example, suppose that a field in the database, named PictureFile, holds the path of a BMP file. You can't directly display this bitmap using a bound control, but you can trap the MoveComplete event and manually load the image into a PictureBox control:
Private Sub Adodc1_MoveComplete(ByVal adReason As ADODB.EventReasonEnum, _ ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset) picPhoto.Picture = LoadPicture(Adodc1.Recordset("PictureFile")) End Sub |
The WillChangeRecord fires immediately before the ADO Data control writes data to the database. This is the best place to validate the contents of bound controls and cancel the operation if necessary:
Dim ValidationError As Boolean ' A form-level variable Private Sub Adodc1_WillChangeRecord(ByVal adReason As _ ADODB.EventReasonEnum, ByVal cRecords As Long, adStatus As _ ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset) ' Check that fields are valid, cancel the update if not. If txtProductName = "" Or Not IsNumeric(txtUnitPrice) Then MsgBox "Please enter valid field values", vbExclamation ValidationError = True adStatus = adStatusCancel End If End Sub |
The Error event is the only event that hasn't been inherited from the Recordset object. The ADO Data control fires it if an error occurs while there's no Visual Basic code running. Typically, this happens when the user clicks on one of the control's arrow buttons and the resulting operation fails (for example, when you're trying to update a record locked by another user). This event also fires when you cancel an operation in the WillChangeRecord event. By default, the ADO Data control displays a standard error message, but you can modify this standard behavior by assigning True to the fCancelDisplay parameter. This code snippet completes the previous example that performs field validation:
Private Sub Adodc1_Error(ByVal ErrorNumber As Long, Description As String,_ ByVal Scode As Long, ByVal Source As String, ByVal HelpFile As String,_ ByVal HelpContext As Long, fCancelDisplay As Boolean) ' Don't show validation errors (already processed elsewhere). If ValidationError Then fCancelDisplay = True ValidationError = False End If End Sub |
One of the serious limitations of the original Data and RemoteData controls was that you couldn't format data for display on screen. For example, if you wanted to display a number with thousand separators, you had to write code. You also had to write code to display a date value in the long date format (such as "October 17, 1999") or some other more or less standard format. Well, it doesn't make sense to use bound controls if you have to write code even for such basic operations, does it?
The ADO binding mechanism effectively and elegantly solves this problem by adding the new DataFormat property to all bound controls. If you click on the DataFormat item in the Properties window, the property page shown in Figure 8-15 appears. Here you can interactively select from many basic format types (such as number, currency, date, and time), each one with several formatting options (number of decimal digits, thousand separators, currency symbol, date/time formats, and so on).
You can also set custom formatting, in which case you can specify a format string that follows the same syntax for the Format function (which I described in Chapter 5). But the ADO Data control can't correctly unformat values that were formatted using a complex format string, so it might store wrong data in the database. This problem can be overcome using StdDataFormat objects, as I explain in the next section.
The DataFormat property might not work correctly for a few controls. For example, when used with the DataCombo control this property doesn't affect the formatting of the items in the list portion. The DataGrid control exposes a DataFormats collection, where each item affects the format of a column of the grid.
Figure 8-15. The DataFormat property page.
To be able to use StdDataFormat objects, you must add a reference to the Microsoft Data Formatting Object Library, and you must then distribute the corresponding MSSTDFMT.DLL file with all your applications.
The first use for these objects is to set the DataFormat property of a bound field at run time. You must do this, for example, when you create controls during execution using a control array (as explained in Chapter 3) or using the new Visual Basic 6 dynamic control creation feature (covered in Chapter 9). To modify how data is formatted in a bound control, you create a new StdDataFormat object, set its properties, and then assign it to the DataFormat property of the control, as in the following piece of code:
Private Sub Form_Load() ' Create a new formatting object and assign it to a bound field. Dim sdf As New StdDataFormat sdf.Type = fmtCustom sdf.Format = "mmm dd, yyyy" Set txtShippedDate.DataFormat = sdf ' Force the Data control to correctly display the first record. Adodc1.Recordset.Move 0 End Sub |
The most important property of StdDataFormat objects is Type, which can be assigned one of the following values: 0-fmtGeneral, 1-fmtCustom, 2-fmtPicture, 3-fmtObject, 4-fmtCheckbox, 5-fmtBoolean, or 6-fmtBytes. If you're using a custom formatting option, you can assign the Format property a formatting string, which has the same syntax as the second argument of the VBA Format function.
When retrieving data from a Boolean field, you typically use a CheckBox control and the frmCheckbox setting. But you can also use a Label or a TextBox control that interprets the contents of the Boolean field and displays a meaningful description. In this case, you must assign strings to the FalseValue, TrueValue, and (optionally) NullValue properties:
Private Sub Form_Load() Dim sdf As New StdDataFormat sdf.Type = frmBoolean ' Set meaningful strings for False, True, and Null values. sdf.FalseValue = "In Production" sdf.TrueValue = "Discontinued" sdf.NullValue = "(unknown)" Set lblDiscontinued.DataFormat = sdf ' Force the Data control to correctly display the first record. Adodc1.Recordset.Move 0 End Sub |
As a rule, you should use this technique only with Label, locked TextBox, and ListBox controls because the user shouldn't be allowed to enter a value other than the three strings assigned to the FalseValue, TrueValue, and NullValue properties.
The real power of StdDataFormat objects is their ability to raise events. You can think of an StdDataFormat object as something that sits between a data source and a data consumer. You're using an ADO Data control as a data source and a bound control as a data consumer in this case, but the StdDataFormat object can be used whenever the ADO binding mechanism is active. StdDataFormat objects let you actively intervene when the data is moved from the data source to the consumer and when the move occurs in the opposite direction. You do this thanks to the Format and Unformat events.
To trap these events, you must declare the StdDataFormat object as a form-level variable, using the WithEvents keyword. The Format event fires when a value is read from the database and displayed in a control. If the user modifies the value, an Unformat event fires when the ADO Data control saves the value back to the database. Both events receive a DataValue parameter, which is an object with two properties: Value is the value being transferred, and TargetObject is the bound control—or more generally, the data consumer—involved. Normally, if you manually format and unformat values inside these events you don't need to set any other property of the StdDataFormat object.
Figure 8-16 shows a sample program (also available on the companion CD) that displays data from the Orders table of the NWind.mdb database. The Freight amount can be expressed as dollars or euros, but the database stores the value only in dollars and therefore you need to do the conversion on the fly. This is the code that does the trick:
' How many euros in one dollar? ' (Of course, in a real program this would be a variable.) Const DOLLARS_TO_EURO = 1.1734 Dim WithEvents sdfFreight As StdDataFormat Private Sub Form_Load() Set sdfFreight = New StdDataFormat Set txtFreight.DataFormat = sdfFreight ' Force the Data control to correctly display the first record. Adodc1.Recordset.Move 0 End Sub Private Sub sdfFreight_Format(ByVal DataValue As StdFormat.StdDataValue) ' Convert to euros if necessary. If optFreight(1) Then DataValue.Value = Round(DataValue.Value * DOLLARS_TO_EURO, 2) End If End Sub Private Sub sdfFreight_UnFormat(ByVal DataValue As StdFormat.StdDataValue) ' Convert back to dollars if necessary. If optFreight(1) Then DataValue.Value = Round(DataValue.Value / DOLLARS_TO_EURO, 2) End If End Sub Private Sub optFreight_Click(Index As Integer) If Index = 0 Then ' Convert from euros to dollars. txtFreight = Trim$(Round(CDbl(txtFreight) / DOLLARS_TO_EURO, 2)) Else ' Convert from dollars to euros. txtFreight = Trim$(Round(CDbl(txtFreight) * DOLLARS_TO_EURO, 2)) End If End Sub |
Figure 8-16. Use StdDataFormat object to keep the display format independent of the value stored in the database.
You can use the DataValue.TargetObject property to affect the appearance or the behavior of the bound control. For example, you can have the Freight amount appear in red when it's higher than 30 dollars:
Private Sub sdfFreight_Format(ByVal DataValue As StdFormat.StdDataValue) ' Show the value in red ink if >30 dollars. If DataValue.Value > 30 Then DataValue.TargetObject.ForeColor = vbRed Else DataValue.TargetObject.ForeColor = vbBlack End If ' Convert to euros if necessary. If optFreight(1) Then DataValue.Value = Round(DataValue.Value * DOLLARS_TO_EURO, 2) End If End Sub |
An interesting—though not adequately documented—feature of StdDataFormat objects is that you can assign the same instance to the DataFormat properties of more than one control. This is especially effective when you want to manage the formatting yourself in Format and Unformat events, so you have a single entry point to format multiple on-screen fields. You can use the DataValue.TargetObject.Name property to find out which bound control is requesting the formatting, as in the piece of code you see here.
Private Sub sdfGeneric_Format(ByVal DataValue As StdFormat.StdDataValue) Select Case DataValue.TargetObject.Name Case "txtFreight", "txtGrandTotal" ' These are currency fields. DataValue.Value = FormatCurrency(DataValue.Value) Case "ProductName" ' Display product names in uppercase characters. DataValue.Value = UCase$(DataValue.Value) End Select End Sub |
An StdDataFormat object exposes a third event, Changed, which fires when any property of the object has been changed. You usually react to this event by refreshing the contents of fields, as follows:
Private Sub sdfGeneric_Changed() ' This forces the ADO Data control to reread the current record. Adodc1.Recordset.Move 0 End Sub |
Here are a few other bits of information about StdDataFormat objects:
The Data Form Wizard is a Visual Basic 6 add-in that automatically builds a form with a group of controls that are bound to a data source. It also creates a number of push buttons for common database operations, such as adding or deleting records. The Data Form Wizard was already included with Visual Basic 5, but it has been modified to work with the ADO Data control. It can also work without an ADO Data control simply by using an ADO Recordset as the data source, and it can even create an ad hoc data source class module.
Using the Data Form Wizard is really straightforward, so I won't describe each step in detail. Figure 8-17 displays an intermediate step, in which you decide the type of form you want to create and which type of binding should be used. You can generate a set of bound controls, a single DataGrid or Hierarchical FlexGrid control, a master/detail form, or an MSChart. The wizard is especially useful for creating single record and master/detail forms. For example, Figure 8-18 shows a form that displays orders in the upper part and information for each order in the lower grid.
Figure 8-17. The Data Form Wizard creates five different types of bound forms.
In the next page, you select which table or view should be used as the record source. You also select which fields will be included in the result form and whether the output is to be sorted on a given column. If you are building a master/detail form, you have to select two different record sources, one for the master table and one for the detail table, and you also need to select one field from each table that links the two sources together. In the Control Selection tab you select which buttons will be placed on the form, choosing from the Add, Update, Delete, Refresh, and Close buttons. Finally, in the last page of the wizard, you have the opportunity to save all your settings in an RWP profile file so that you can later rerun the wizard without having to reenter all the options.
Figure 8-18. A master/detail form based on the Orders and Order Details tables of the NWind.mdb database.
It's highly likely that you'll need to fix the form or the code produced by the Data Form Wizard, if only for cosmetic reasons. But you might learn how to write good ADO code by simply running it and then browsing the results. Or you could use the wizard just to create a data source class. (Data source classes are described in Chapter 18.)